/*
 * MLXQService.java
 */

package xq.marklogic;

import com.marklogic.xcc.ContentSource;
import com.marklogic.xcc.Request;
import com.marklogic.xcc.ResultItem;
import com.marklogic.xcc.ResultSequence;
import com.marklogic.xcc.Session;
import com.marklogic.xcc.exceptions.XQueryException;
import com.marklogic.xcc.exceptions.XQueryStackFrame;
import com.marklogic.xcc.types.ItemType;
import java.io.InputStream;
import java.io.StringReader;
import java.util.List;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import xq.api.XQException;
import xq.api.XQRequest;
import xq.api.XQResponse;
import xq.api.XQService;
import xq.api.XQVariable;
import static xq.util.CommonUtil.empty;

public class MLXQService implements XQService {

	private ContentSource contentSource;

	public MLXQService() {
		super();
	}
	
	/*--------------------------------------------------------------------
	 * Property Getters/Setters
	 *--------------------------------------------------------------------*/
	
	public void setContentSource(ContentSource contentSource) {
		this.contentSource = contentSource;
	}

	/*--------------------------------------------------------------------
	 * Public Implementation
	 *--------------------------------------------------------------------*/

	public XQResponse<String> queryToString(XQRequest request) throws XQException {
		MLXQResponse<String> response = new MLXQResponse<String>();
		String result = executeQuery(request, response).asString();
		response.setResult(result);
		return response;
	}

	public XQResponse<String[]> queryToStringArray(XQRequest request) throws XQException {
		MLXQResponse<String[]> response = new MLXQResponse<String[]>();
		ResultSequence rs = executeQuery(request, response);
		try {
			ResultItem[] resultItems = rs.toResultItemArray();
			if (!empty(resultItems)) {
				String[] results = new String[resultItems.length];
				for (int i = 0; i < resultItems.length; i++) {
					results[i] = resultItems[i].asString();
				}
				response.setResult(results);
			}
			return response;
		} finally {
			rs.close();
		}
	}

	public XQResponse<InputStream[]> queryToStream(XQRequest request) throws XQException {
		MLXQResponse<InputStream[]> response = new MLXQResponse<InputStream[]>();
		ResultSequence rs = executeQuery(request, response);
		try {
			ResultItem[] resultItems = rs.toResultItemArray();
			if (!empty(resultItems)) {
				InputStream[] resultStreams = new InputStream[resultItems.length];
				for (int i = 0; i < resultItems.length; i++) {
					resultStreams[i] = resultItems[i].asInputStream();
				}
				response.setResult(resultStreams);
			}
			return response;
		} finally {
			rs.close();
		}
	}

	public <T> XQResponse<T> queryToObject(XQRequest request, Class<T> resultType) throws XQException, JAXBException {
		JAXBContext binding = JAXBContext.newInstance(resultType);
		MLXQResponse<T> response = new MLXQResponse<T>();
		ResultSequence rs = executeQuery(request, response);
		String xmlResult = rs.asString();
		Unmarshaller um = binding.createUnmarshaller();
		T result = resultType.cast(um.unmarshal(new StringReader(xmlResult)));
		response.setResult(result);
		return response;
	}

	/*--------------------------------------------------------------------
	 * Private Implementation
	 *--------------------------------------------------------------------*/

	private ResultSequence executeQuery(XQRequest request, MLXQResponse response) throws XQException {
		try {
			Session session = contentSource.newSession();
			try {
				Request req = session.newAdhocQuery(request.getXQuery().getQueryText());
				setExternalVariables(req, request);
				long start = System.currentTimeMillis();
				ResultSequence result = session.submitRequest(req);
				response.setDuration(System.currentTimeMillis() - start);
				return result;
			} finally {
				session.close();
			}
		} catch (RuntimeException ex) {
			throw ex;
		} catch (XQException ex) {
			throw ex;
		} catch (XQueryException ex) {
			XQueryStackFrame[] frames = ex.getStack();
			if (!empty(frames)) {
				List<String> lines = request.getXQuery().getLines();
				String code = ex.getCode();
				String msg = ex.getFormatString();
				int line = frames[0].getLineNumber() - 1;
				int startLine = Math.max(0, line - 2);
				int endLine = Math.min(lines.size(), line + 3);
				StringBuilder sb = new StringBuilder();
				sb.append(request.getXQuery().getLocationString(line)).append("\n\n");
				sb.append(code).append(": ").append(msg).append("\n\n");
				sb.append("XQuery stack trace:\n\n");
				sb.append("line ").append(line + 1).append(":\n");
				for (int i = startLine; i < endLine; i++) {
					sb.append(i + 1).append(": ").append(lines.get(i)).append('\n');
				}
				throw new XQException(sb.toString(), ex);
			} else {
				throw new XQException(ex);
			}
		} catch (Exception ex) {
			throw new XQException(ex);
		}
	}

	private void setExternalVariables(Request req, XQRequest request) throws XQException {
		for (XQVariable variable : request.getXQuery().getExternalVariables()) {
			String name = variable.getName();
			String type = variable.getType();
			Object value = request.getVariableInput(name);
			ItemType itemType = ItemType.forType(type);
			if (itemType == null) {
				throw new XQException("External variable \"" + variable + "\" is of an unsupported type.");
			} else if (value == null) {
				throw new XQException("External variable \"" + variable + "\" requires a value.");
			} else {
				try {
					req.setNewVariable(name, itemType, value);
				} catch (Exception ex) {
					throw new XQException("Failed to set external variable \"" + variable + "\": " + ex.getMessage());
				}
			}
		}
	}

	private static class MLXQResponse<T> implements XQResponse<T> {

		private long duration;
		private T result;

		public long getDuration() {
			return duration;
		}

		private void setDuration(long duration) {
			this.duration = duration;
		}

		public T getResult() {
			return result;
		}

		private void setResult(T result) {
			this.result = result;
		}
	}
}
